﻿using System.Collections.Generic;
using System.IO;
using System.Text;

namespace AdvplDoc.Model {

	class Parser {
		public List<AdvplFunction> functions { get; set; }
		public Dictionary<string, AdvplClass> classes { get; set; }
		public AdvplClass currentClass { get; set; }

		public Parser() {
			this.functions = new List<AdvplFunction>();
            this.classes = new Dictionary<string, AdvplClass>();
			this.currentClass = null;
		}

        internal void parse(List<string> files, System.Action<int> start, System.Action<int> update) {
            int total = files.Count;

            start(total);

            for (int i = 0; i < total; i++) {
                update(i);

                this.parse(files[i]);
            }

            this.updateParentClasses();
        }

        internal void parse(string file) {
            string text = this.getUtfText(file);

            /*
            using (StreamReader reader = new System.IO.StreamReader(file, true)) {
                System.Console.WriteLine("\n" + file + " " + reader.CurrentEncoding.BodyName + "\n");


                Encoding currentEncoding = reader.CurrentEncoding;
                string currText = reader.ReadToEnd();

                byte[] currBytes = currentEncoding.GetBytes(currText);
                byte[] utf8Bytes = Encoding.Convert(currentEncoding, Encoding.UTF8, currBytes);
                text = Encoding.UTF8.GetString(utf8Bytes);
            }
            */

            //string text = File.ReadAllText(file, );

            //try {
            this.parseLines(text.Split('\n'));
            //}
            //catch (Exception ex) {
            //System.Console.WriteLine("\nErro no arquivo: " + file + "\n");
            //System.Console.WriteLine(ex.ToString());
            //}
        }

		public void parseLines(string[] lines) {
			this.parseLines(new List<string>(lines));
		}

		public void parseLines(List<string> lines) {
			List<string> tokens = this.split_by_char(lines, ' ');
			tokens = this.split_by_char(tokens, '\t');
			tokens = this.split_by_char(tokens, ';');
			tokens = this.split_by_char(tokens, '(');
			tokens = this.split_by_char(tokens, ')');
			tokens = this.split_by_char(tokens, '{');

			tokens = this.remove_empty(tokens);

            
            int pos = tokens.IndexOf("tClassInterface");
            while (pos >= 0) {
                string name = "";
                string parent = "";
                try {
                    name = tokens[pos + 1].Split('"')[1];
                    parent = tokens[pos + 2].Split('"')[1];

                    this.addClass(new AdvplClass(name, parent));
                }
                catch {
                    System.Console.WriteLine("Error parsing: " + name);
                }

                pos = tokens.IndexOf("tClassInterface", pos+1);
            }
            

			//current file loop
			int i = 0;
			while (i < tokens.Count) {
				//check advpldoc tags inside comment

				if (tokens[i] == "/**") {
					while ((tokens[i] != "@advpl_class" &&
                            tokens[i] != "@advpl_constructor" &&
                            tokens[i] != "@advpl_method" &&
							tokens[i] != "@advpl_attribute" &&
							tokens[i] != "@advpl_function") &&
							!tokens[i].EndsWith("*/")) {
						i++;
					}

					if (tokens[i] == "@advpl_class") {
                        i = this.parse_class(tokens, i);
					}
                    else if (tokens[i] == "@advpl_constructor") {
                        if (this.currentClass == null)
                            System.Console.WriteLine("WARNING: Found @advpl_constructor token before @advpl_class. Skipping.");
                        else
                            i = this.parse_method(tokens, i, true);
                    }
					else if (tokens[i] == "@advpl_method") {
						if (this.currentClass == null)
							System.Console.WriteLine("WARNING: Found @advpl_method token before @advpl_class. Skipping.");
						else
                            i = this.parse_method(tokens, i);
					}
					else if (tokens[i] == "@advpl_attribute") {
						if (this.currentClass == null)
							System.Console.WriteLine("WARNING: Found @advpl_attribute token before @advpl_class. Skipping.");
						else
							i = this.parse_property(tokens, i);
					}
					else if (tokens[i] == "@advpl_function") {
                        i = this.parse_function(tokens, i);
					}
					else if (tokens[i].StartsWith("@") && tokens[i].Length > 1) {
						System.Console.WriteLine("WARNING: Unknown token in parse_tokens: " + tokens[i]);
					}
				}

				i++;
			}
		}

		private List<string> split_by_char(List<string> tokens, char delimiter) {
			bool is_in_pre = false;
			List<string> new_tokens = new List<string>();

			foreach (string token in tokens) {
				if (token.Trim() == Constants.PRE_BEGIN_TAG || token.Trim() == Constants.PRE_END_TAG) {
					is_in_pre = !is_in_pre;
					new_tokens.Add(token.Trim());
				}
				else {
					if (is_in_pre) {
						new_tokens.Add(token.Replace("\r", ""));
					}
					else {
						string[] tokens_aux = token.Trim().Split(delimiter);
						new_tokens.AddRange(tokens_aux);
					}
				}
			}

			return new_tokens;
		}

		private List<string> remove_empty(List<string> tokens) {
			bool is_in_pre = false;
			List<string> new_tokens = new List<string>();
			
			foreach (string token in tokens) {
				string t =  token.Trim();

				//if (t == "*")
					//continue;
				
				if ((t == Constants.PRE_BEGIN_TAG) ||
					(t == Constants.PRE_END_TAG))
					is_in_pre = !is_in_pre;

				if (is_in_pre || (!is_in_pre && token != ""))
					new_tokens.Add(token);
			}

			return new_tokens;
		}

		private string parse_pre(List<string> tokens, int i) {
			string pre = "";

			while (tokens[i] != Constants.PRE_END_TAG) 
				pre = pre + tokens[i] + '\n';

			return pre;
		}

		private string parse_str(List<string> tokens, int i) {
			string mystr = "";
			bool is_in_pre = false;

			while ((!tokens[i].EndsWith("*/")) && (!tokens[i].StartsWith("@advpl_"))) {
				if (tokens[i].Trim() == Constants.PRE_BEGIN_TAG ||
					tokens[i].Trim() == Constants.PRE_END_TAG) {
					is_in_pre = !is_in_pre;
					mystr = mystr.TrimEnd() + '\n' + tokens[i] + '\n';
					i++;
					continue;
				}

				if (is_in_pre) {
					mystr = mystr + tokens[i] + '\n';
				}
				else {
					if (tokens[i] != "*")
                        mystr = mystr + tokens[i] + ' ';
				}

				i++;
			}

			return mystr.Trim();
		}

		private AdvplParam parse_param(List<string> tokens, int i) {
			string param_name = "";
			string param_desc = "";
			int pos = 0;

			i++;

			while ((!tokens[i].EndsWith("*/")) && (!tokens[i].StartsWith("@advpl_"))) {
				if (tokens[i] != "*") {
					if (pos == 0) {
						param_name = tokens[i];
						pos = pos + 1;
					}
					else if  (pos == 1) {
                        param_desc = this.parse_str(tokens, i);
						pos = pos + 1;
					}
				}

				i++;
			}
						
			return new AdvplParam(param_name, param_desc);
		}

		private AdvplReturnInfo parse_return(List<string> tokens, int i) {
			AdvplType type = new AdvplType("");
			string name = "";
			string desc = "";
			int pos = 0;
			i++;

			while ((!tokens[i].EndsWith("*/")) && (!tokens[i].StartsWith("@advpl_"))) {
				if (tokens[i] != "*") {
					if (pos == 0) {
						type = new AdvplType(tokens[i]);
						pos++;
					}
					else if (pos == 1) {
						name = tokens[i];
						pos++;
					}
					else {
						desc = desc + tokens[i] + " ";
					}
				}

				i++;
			}

			desc = desc.Trim();

			return new AdvplReturnInfo(type, name, desc);
		}

		private int parse_class(List<string> tokens, int i) {
			string desc = "";
			List<string> obs_list = new List<string>();
			string since = "";
			bool is_deprecated = false;
			string deprecated_info = "";
			List<string> seealso = new List<string>();
			List<string> examples = new List<string>();

			i++;

			//for (i in range(i, tokens.Count)) {
			//while (i < tokens.Count) {
			for (; i < tokens.Count; i++) {
				if (tokens[i].EndsWith("*/"))
					break;

				if (tokens[i] == "@advpl_desc") {
					i++;
                    desc = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_obs") {
					i++;
                    string obs = this.parse_str(tokens, i);
					obs_list.Add(obs);
				}
				else if (tokens[i] == "@advpl_example") {
					i++;
                    string example = this.parse_str(tokens, i);
					examples.Add(example);
				}
				else if (tokens[i] == "@advpl_since") {
					i++;
                    since = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_deprecated") {
					is_deprecated = true;
					i++;
                    deprecated_info = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_seealso") {
					i++;
                    seealso.Add(this.parse_str(tokens, i));
				}
				else if (tokens[i].StartsWith("@") && tokens[i].Length > 1) {
					System.Console.WriteLine("WARNING: Unknown token in parse_class: " + tokens[i]);
				}
			}

            //string parent = tokens[i + 1].Split('"')[1];

			for (; i < tokens.Count; i++) {
                string name = "";
                string parent = "";

                if ((tokens[i] == "class") &&
                    (tokens[i+2] == "public")) {
                    name = tokens[i + 1].Trim(':');
                    parent = tokens[i + 3];
                }
                else if (tokens[i] == "tClassInterface") {
                    i++;
                    name = tokens[i].Split('"')[1];
                    parent = tokens[i + 1].Split('"')[1];
                }

                if (name != "") {
                    this.currentClass = this.getClass(name);

                    if (this.currentClass != null) {
                        this.currentClass.parent = parent;
                        this.currentClass.description = desc;
                        this.currentClass.observation = obs_list;
                        this.currentClass.examples = examples;
                        this.currentClass.since = since;
                        this.currentClass.isDeprecated = is_deprecated;
                        this.currentClass.deprecatedInfo = deprecated_info;
                        this.currentClass.seeAlso = seealso;
                    }
                    else {
                        this.currentClass = new AdvplClass( name,
                                                            parent,
                                                            desc,
                                                            obs_list,
                                                            examples,
                                                            since,
                                                            is_deprecated,
                                                            deprecated_info,
                                                            seealso);

                        this.addClass(this.currentClass);
                    }

					break;
				}
			}

			return i;
		}

		private int parse_method(List<string> tokens, int i, bool constructor = false) {
			AdvplReturnInfo return_info = null;
			string desc = "";
			List<string> obs_list = new List<string>();
			List<AdvplParam> param_list = new List<AdvplParam>();
			List<string>examples = new List<string>();
			string since = "";
			bool is_deprecated = false;
			string deprecated_info = "";
			List<string> seealso = new List<string>();

			i++;
			// range
			for (; i < tokens.Count; i++) {
				if (tokens[i].EndsWith("*/")) {
					break;
				}

				if (tokens[i] == "@advpl_return") {
                    return_info = this.parse_return(tokens, i);
				}
				else if ( tokens[i] == "@advpl_desc") {
					i++;
                    desc = this.parse_str(tokens, i);
				}
				else if ( tokens[i] == "@advpl_obs") {
					i++;
                    string obs = this.parse_str(tokens, i);
					obs_list.Add(obs);
				}
				else if ( tokens[i] == "@advpl_param") {
                    AdvplParam param = this.parse_param(tokens, i);
					param_list.Add(param);
				}
				else if ( tokens[i] == "@advpl_example") {
					i++;
                    examples.Add(this.parse_str(tokens, i));
				}
				else if ( tokens[i] == "@advpl_since") {
					i++;
                    since = this.parse_str(tokens, i);
				}
				else if ( tokens[i] == "@advpl_deprecated") {
					is_deprecated = true;
					i++;
                    deprecated_info = this.parse_str(tokens, i);
				}
				else if ( tokens[i] == "@advpl_seealso") {
					i++;
                    seealso.Add(this.parse_str(tokens, i));
				}
				else if ( tokens[i].StartsWith("@") && tokens[i].Length > 1) {
					System.Console.WriteLine("WARNING: Unknown token in parse_method: " + tokens[i]);
				}
			}

			for (; i < tokens.Count; i++) {
				if (tokens[i] == "tMethod") {
					string name = tokens[i + 2].Split('"')[1];
					string raw_params = tokens[i + 4].Split('"')[1];

					if ((raw_params.Length / 2) < param_list.Count) {
						System.Console.WriteLine("WARNING: Found more @advpl_param tags than real params for method " + name + ".");
					}

					int k = 0;
					for (int j = 0; j < raw_params.Length; j += 2) {
						if (param_list.Count > k) {
							param_list[k].type = new AdvplType(raw_params[j]);
							param_list[k].isRequired = (raw_params[j + 1] == 'R');
							k++;
						}
						else {
							AdvplParam param = new AdvplParam("", "");
							param.type = new AdvplType(raw_params[j]);
							param.isRequired = (raw_params[j + 1] == 'R');
							param_list.Add(param);
						}
					}

					AdvplMethod advplMethod = new AdvplMethod(	name,
																desc,
																param_list,
																return_info,
																obs_list,
																examples,
																since,
																is_deprecated,
																deprecated_info,
																seealso);
                    if (constructor)
                        this.currentClass.addConstructor(advplMethod);
                    else
                        this.currentClass.addMethod(advplMethod);

					i += 4;
					break;
				}
			}

			return i;
		}

		private int parse_property(List<string> tokens, int i) {
			string desc = "";
			AdvplType type = new AdvplType("");
            List<string> observations = new List<string>();
			string since = "";
			bool is_deprecated = false;
			string deprecated_info = "";
			List<string> seealso = new List<string>();
            string defaultValue = "";

			i++;
			//for (i in range(i, len(tokens))) {
			for (; i < tokens.Count; i++) {
				if (tokens[i].EndsWith("*/"))
					break;

				if (tokens[i] == "@advpl_type") {
					i++;
					type = new AdvplType(this.parse_str(tokens, i));
				}
				else if (tokens[i] == "@advpl_desc") {
					i++;
					desc = this.parse_str(tokens, i);
				}
                else if (tokens[i] == "@advpl_obs") {
                    i++;
                    observations.Add(this.parse_str(tokens, i));
                }
                else if (tokens[i] == "@advpl_since") {
                    i++;
                    since = this.parse_str(tokens, i);
                }
                else if (tokens[i] == "@advpl_deprecated") {
                    is_deprecated = true;
                    i++;
                    deprecated_info = this.parse_str(tokens, i);
                }
                else if (tokens[i] == "@advpl_seealso") {
                    i++;
                    seealso.Add(this.parse_str(tokens, i));
                }
                else if (tokens[i] == "@advpl_defaultvalue") {
                    i++;
                    defaultValue = this.parse_str(tokens, i);
                }
                else if (tokens[i].StartsWith("@") && tokens[i].Length > 1) {
                    System.Console.WriteLine("WARNING: Unknown token in parse_attribute: " + tokens[i]);
                }
			}


			//for (i in range(i, len(tokens))) {
			for (; i < tokens.Count; i++) {
                string name = "";

                if (tokens[i] == "/*$PROPERTY") {
					i += 2;
					name = tokens[i];
                }
                else if (tokens[i] == "tClassData") {
                    i += 2;
					name = tokens[i].Split('"')[1];
                }

                if (name != "") {
					AdvplProperty advplProperty = new AdvplProperty(type, 
																	name, 
																	desc, 
                                                                    observations,
																	since,
																	is_deprecated, 
																	deprecated_info,
																	seealso,
                                                                    defaultValue);
					this.currentClass.addProperty(advplProperty);
					break;
				}
			}

			return i;
		}

		private int parse_function(List<string> tokens, int i) {
			AdvplReturnInfo return_info = null;
			string desc = "";
			List<string> obs_list = new List<string>();
			List<AdvplParam> param_list = new List<AdvplParam> ();
			List<string> examples = new List<string>();
			string since = "";
			bool is_deprecated = false;
			string deprecated_info = "";
			List<string> seealso = new List<string>();

			i++;
			
			//for i in range(i, len(tokens)):
			for (; i < tokens.Count; i++) {
				if (tokens[i].EndsWith("*/"))
					break;

				if (tokens[i] == "@advpl_return") {
					return_info = this.parse_return(tokens, i);
				}
				else if (tokens[i] == "@advpl_desc") {
					i++;
					desc = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_obs") {
					i++;
					string obs = this.parse_str(tokens, i);
					obs_list.Add(obs);
				}
				else if (tokens[i] == "@advpl_param") {
					AdvplParam param = this.parse_param(tokens, i);
					param_list.Add(param);
				}
				else if (tokens[i] == "@advpl_example") {
					i++;
					string example = this.parse_str(tokens, i);
					examples.Add(example);
				}
				else if (tokens[i] == "@advpl_since") {
					i++;
					since = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_deprecated") {
					is_deprecated = true;
					i++;
					deprecated_info = this.parse_str(tokens, i);
				}
				else if (tokens[i] == "@advpl_seealso") {
					i++;
					seealso.Add(this.parse_str(tokens, i));
				}
				else if (tokens[i].StartsWith("@") && tokens[i].Length > 1) {
					System.Console.WriteLine("WARNING: Unknown token in parse_function: " + tokens[i]);
				}
			}

			string name = "";
			string raw_params = "";

			int tfunction_counter = 0;

			//for i in range(i, len(tokens)):
			for (; i < tokens.Count; i++) {
				if (tokens[i] == "tFunction") {
					i++;
					tfunction_counter++;

					if (tfunction_counter != 2)
						continue;
					else
						raw_params = tokens[i].Split('"')[1];

					if ((raw_params.Length / 2) < param_list.Count)
						System.Console.WriteLine("WARNING: Found more @advpl_param tags than real params for tFunction [" + name + "].");

					int k = 0;
					for (int j = 0; j < raw_params.Length; j +=2) {
						if (param_list.Count > k) {
							param_list[k].type = new AdvplType(raw_params[j]);
							param_list[k].isRequired = (raw_params[j+1] == 'R');
							k++;
						}
						else {
							AdvplParam param = new AdvplParam("", "");
							param.type = new AdvplType(raw_params[j]);
							param.isRequired = (raw_params[j+1] == 'R');
							param_list.Add(param);
						}
					}

					i++;
					continue;
				}
				else if ((tokens[i] == "name") && (tokens[i+1] == "return")) {
					name = tokens[i + 2].Split('"')[1];
					i += 3;
					continue;
				}
				else if (name != "" && tfunction_counter > 1) {
					AdvplFunction advplFunction = new AdvplFunction(	name, 
																		desc, 
																		param_list,
																		return_info, 
																		obs_list, 
																		examples,
																		since, 
																		is_deprecated,
																		deprecated_info, 
																		seealso);
					this.functions.Add(advplFunction);
					break;
				}
				else {
					i++;
				}
			}

			return i;
		}


 		private string getUtfText(string file) {
			//"utf-8"
			//"iso-8859-1"
			Encoding utfEncoding = Encoding.UTF8;
			Encoding isoEncoding = Encoding.GetEncoding("iso-8859-1");
			
			using (StreamReader reader = new System.IO.StreamReader(file, isoEncoding)) {
				//System.Console.WriteLine("\n" + file + " " + reader.CurrentEncoding.BodyName + "\n");
				Encoding currentEncoding = reader.CurrentEncoding;

				string currentText = reader.ReadToEnd();
				byte[] currentBytes = currentEncoding.GetBytes(currentText);

				byte[] utfBytes = Encoding.Convert(currentEncoding, utfEncoding, currentBytes);
				byte[] isoBytes = Encoding.Convert(currentEncoding, isoEncoding, currentBytes);

				string utfString = utfEncoding.GetString(utfBytes);
				string isoString = utfEncoding.GetString(isoBytes);


				if ((currentText == utfString) && (currentText != isoString)) {
					//System.Console.WriteLine("\n" + file + " = UTF8");
					return utfString;
				}
				else if ((currentText != utfString) && (currentText == isoString)) {
					//System.Console.WriteLine("\n" + file + " = ISO");
					return isoString;
				}
				else if ((currentText == utfString) && (currentText == isoString)) {
					//System.Console.WriteLine("\n" + file + " = UTF8+ISO");
					return utfString;
				}
				else {
					//System.Console.WriteLine("\n" + file + " = NONE");
					return currentText;
				}
			}
		}


        private void updateParentClasses() {

            foreach (AdvplClass c in this.getClassList()) {
                if ((c.parent != "") && this.hasClass(c.parent)) {
                    c.parentClass = this.getClass(c.parent);
                }
            }
        }

        public void addClass(AdvplClass c) {
            this.classes.Add(c.name.ToUpper(), c);
        }

        public AdvplClass getClass(string name) {
            string key = name.ToUpper();

            if (this.classes.ContainsKey(key))
                return classes[key];

            return null;
        }

        public bool hasClass(string name) {
            return (this.classes.ContainsKey(name.ToUpper()));
        }

        public List<AdvplClass> getClassList() {
            return new List<AdvplClass>(this.classes.Values);
        }
    }		
			 
}
